#include "GL/glew.h"
#include "terrain.h"

inline const GLubyte *BUFFER_OFFSET(size_t bytes)
{ return reinterpret_cast<const GLubyte *>(0) + bytes; }

Terrain::Terrain()
{
    bufferVertex = 0;
    bufferIndex = 0;
    numVertices = 0;
    numIndices = 0;
}

Terrain::~Terrain()
{
    destroy();
}

bool Terrain::create(int size, int gridSpacing, float scale)
{
    if (!map.create(size, gridSpacing, scale))
        return false;

    return terrainCreate(size, gridSpacing, scale);
}

void Terrain::destroy()
{
    map.destroy();
    terrainDestroy();
}

void Terrain::draw()
{
    drawMap();
}

bool Terrain::createMap(float roughness)
{
    map.createNewFractal(roughness);
    return generateVertices();
}

bool Terrain::terrainCreate(int size, int gridSpacing, float scale)
{
    // Initialize the vertex buffer

    numVertices = size * size;
    glGenBuffers(1, &bufferVertex);
    glBindBuffer(GL_ARRAY_BUFFER, bufferVertex);
    glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * numVertices,0, GL_DYNAMIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // Initialize the index buffer

    numIndices = (size - 1) * (size * 2 + 1);
    glGenBuffers(1, &bufferIndex);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bufferIndex);
    
    int indexSize = use16BitIndices() ? sizeof(unsigned short) : sizeof(unsigned int);

    glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexSize * numIndices, 0, GL_STATIC_DRAW);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    return generateIndices();
}

void Terrain::terrainDestroy()
{
    if (bufferVertex)
    {
        glDeleteBuffers(1, &bufferVertex);
        bufferVertex = 0;
        numVertices = 0;
    }

    if (bufferIndex)
    {
        glDeleteBuffers(1, &bufferIndex);
        bufferIndex = 0;
        numIndices = 0;
    }
}

void Terrain::drawMap()
{
	// Super ultra optimized draw method to avoid the low fps problem: Thanks internets!

    glBindBuffer(GL_ARRAY_BUFFER, bufferVertex);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bufferIndex);

    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), BUFFER_OFFSET(6 * sizeof(float)));

    glEnableClientState(GL_NORMAL_ARRAY);
    glNormalPointer(GL_FLOAT, sizeof(Vertex), BUFFER_OFFSET(3 * sizeof(float)));

    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(3, GL_FLOAT, sizeof(Vertex), BUFFER_OFFSET(0));

    if (use16BitIndices())
        glDrawElements(GL_TRIANGLE_STRIP, numIndices, GL_UNSIGNED_SHORT, BUFFER_OFFSET(0));
    else
        glDrawElements(GL_TRIANGLE_STRIP, numIndices, GL_UNSIGNED_INT, BUFFER_OFFSET(0));

    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}

bool Terrain::generateIndices()
{
    void *pBuffer = 0;
    int size = map.getSize();

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bufferIndex);
    
    if (!(pBuffer = glMapBuffer(GL_ELEMENT_ARRAY_BUFFER, GL_WRITE_ONLY)))
    {
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
        return false;
    }

    if (use16BitIndices())
    {
        unsigned short *pIndex = static_cast<unsigned short *>(pBuffer);

        for (int z = 0; z < size - 1; ++z)
        {
            if (z % 2 == 0)
            {
                for (int x = 0; x < size; ++x)
                {
                    *pIndex++ = static_cast<unsigned short>(x + z * size);
                    *pIndex++ = static_cast<unsigned short>(x + (z + 1) * size);
                }

                // Add degenerate triangles to stitch strips together
                *pIndex++ = static_cast<unsigned short>((size - 1) + (z + 1) * size);
            }
            else
            {
                for (int x = size - 1; x >= 0; --x)
                {
                    *pIndex++ = static_cast<unsigned short>(x + z * size);
                    *pIndex++ = static_cast<unsigned short>(x + (z + 1) * size);
                }

                // Add degenerate triangles to stitch strips together
                *pIndex++ = static_cast<unsigned short>((z + 1) * size);
            }
        }
    }
    else
    {
        unsigned int *pIndex = static_cast<unsigned int *>(pBuffer);

        for (int z = 0; z < size - 1; ++z)
        {
            if (z % 2 == 0)
            {
                for (int x = 0; x < size; ++x)
                {
                    *pIndex++ = x + z * size;
                    *pIndex++ = x + (z + 1) * size;
                }

                // Add degenerate triangles to stitch strips together
                *pIndex++ = (size - 1) + (z + 1) * size;
            }
            else
            {
                for (int x = size - 1; x >= 0; --x)
                {
                    *pIndex++ = x + z * size;
                    *pIndex++ = x + (z + 1) * size;
                }

                // Add degenerate triangles to stitch strips together
                *pIndex++ = (z + 1) * size;
            }
        }
    }   

    glUnmapBuffer(GL_ELEMENT_ARRAY_BUFFER);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    return true;
}

bool Terrain::generateVertices()
{
    Vertex *pVertices = 0;
    Vertex *pVertex = 0;
    int currVertex = 0;
    int size = map.getSize();
    int gridSpacing = map.getGridSpacing();
    float heightScale = map.getHeightScale();
    Vector3 normal;

    glBindBuffer(GL_ARRAY_BUFFER, bufferVertex);
    pVertices = static_cast<Vertex *>(glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY));

    if (!pVertices)
    {
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        return false;
    }

    for (int z = 0; z < size; ++z)
    {
        for (int x = 0; x < size; ++x)
        {
            currVertex = z * size + x;
            pVertex = &pVertices[currVertex];

            pVertex->x = static_cast<float>(x * gridSpacing);
            pVertex->y = map.heightAtPixel(x, z) * heightScale;
            pVertex->z = static_cast<float>(z * gridSpacing);

            map.normalAtPixel(x, z, normal);
            pVertex->nx = normal.x;
            pVertex->ny = normal.y;
            pVertex->nz = normal.z;
            
            pVertex->s = static_cast<float>(x) / static_cast<float>(size);
            pVertex->t = static_cast<float>(z) / static_cast<float>(size);
        }
    }

    glUnmapBuffer(GL_ARRAY_BUFFER);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    return true;
}
